{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "71c3b6a8-98ef-464c-b3fe-dcc61f8caa52",
   "metadata": {},
   "source": [
    "# Cloning via Offline Backups\n",
    "\n",
    "Cloning items from one organization has long been an important workflow in the ArcGIS API for Python, primarily through [`clone_items()`](/python/latest/guide/cloning-content/). The [`clone_items()`](/python/latest/api-reference/arcgis.gis.toc.html#arcgis.gis.ContentManager.clone_items) method depends on having multiple active GIS connections, and items are dissected and reconstructed from scratch within each call. The [`OfflineContentManager`](/python/latest/api-reference/arcgis.gis.toc.html#offlinecontentmanager) (OCM) module, introduced in version 2.4.1, takes a slightly different approach. While both can take part and reassemble items elsewhere, the `OfflineContentManager` also creates a compressed backup of the items along the way, including all dependencies of the items being cloned.\n",
    "\n",
    "See the the API reference documentation for the module [here](https://developers.arcgis.com/python/latest/api-reference/arcgis.gis.toc.html#offlinecontentmanager).\n",
    "\n",
    "The module is compact, and an object of the `OfflineContentManager` class can be accessed through the [`offline`](/python/latest/api-reference/arcgis.gis.toc.html#arcgis.gis.ContentManager.offline) property of a [ContentManager](/python/latest/api-reference/arcgis.gis.toc.html#contentmanager) object: `gis.content.offline`. It consists of 3 methods: \n",
    "* [`export_items()`](/python/latest/api-reference/arcgis.gis.toc.html#arcgis.gis.OfflineContentManager.export_items) - to export item content to a package\n",
    "* [`import_content()`](/python/latest/api-reference/arcgis.gis.toc.html#arcgis.gis.OfflineContentManager.import_content) - one to import package content \n",
    "* [`list_items()`](/python/latest/api-reference/arcgis.gis.toc.html#arcgis.gis.OfflineContentManager.list_items) - to explore the content of a compressed backup.\n",
    "\n",
    "The way it works behind the scenes is:\n",
    "- In `export_items()`, an [`ItemGraph`](/python/latest/api-reference/arcgis.apps.itemgraph.html#itemgraph) is assembled with all of the items and the items they need to exist\n",
    "- An output folder is generated, and within that, a folder for each item is created with the necessary data and metadata. Services without an underlying feature class file are exported to a File Geodatabase by default, or the format specified in the export call's `service_format` argument.\n",
    "- That output folder is compressed and stored either in the specified path or in a temporary directory\n",
    "- On import, the the package is decompressed, the graph is recreated in memory, and the items are created in order from least dependencies to most\n",
    "\n",
    "There are a few situations where it may be advantageous to use this module instead of `clone_items()`. Creating a singular backup to use from a source org can avoid repeated calls to `clone_items()` in cases where content is being brought to multiple different GIS orgs. Additionally, creating backup packages provides safety against unexpected changes to an organization's content. Another notable difference between the OCM and `clone_items()` is that the `import_content()` method provides the option to skip over items that aren't recreated correctly, as opposed to rolling everything back like `clone_items()` does.\n",
    "\n",
    "Let's take a look at its basic usage, and then a couple more specific use cases."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e27ffdf0-eebd-4f8c-bd4a-bd5a9fc14438",
   "metadata": {},
   "source": [
    "## Step 1: Exporting\n",
    "\n",
    "We'll start by showing how to create one of these offline packages in our source organization. It's as easy as just selecting the items you wish to export, and passing them into the `export_items()` method. A package with those items and every other item they need to exist will be assembled.\n",
    "\n",
    "Let's start by connecting to a GIS, and accessing an OfflineContentManager object."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "0b1df0b5-dbef-4fed-9756-09d5982088c4",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<arcgis.gis.OfflineContentManager at 0x10d35d950>"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from arcgis.gis import GIS\n",
    "gis = GIS(profile=\"your_admin_profile\")\n",
    "ocm = gis.content.offline\n",
    "ocm"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7c70c162-f9d9-479a-836a-4ada1fedebae",
   "metadata": {},
   "source": [
    "We'll get the item that we want to back up and/or migrate."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "a1d32822-9ac8-4891-bce1-abc2c455afb8",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div class=\"item_container\" style=\"height: auto; overflow: hidden; border: 1px solid #cfcfcf; border-radius: 2px; background: #f6fafa; line-height: 1.21429em; padding: 10px;\">\n",
       "                    <div class=\"item_left\" style=\"width: 210px; float: left;\">\n",
       "                       <a href='https://geosaurus.maps.arcgis.com/home/item.html?id=b2136e64f7414d10b02b52b009d7c13b' target='_blank'>\n",
       "                        <img src='' width='200' height='133' class=\"itemThumbnail\">\n",
       "                       </a>\n",
       "                    </div>\n",
       "\n",
       "                    <div class=\"item_right\"     style=\"float: none; width: auto; overflow: hidden;\">\n",
       "                        <a href='https://geosaurus.maps.arcgis.com/home/item.html?id=b2136e64f7414d10b02b52b009d7c13b' target='_blank'><b>Oceanic Experience</b>\n",
       "                        </a>\n",
       "                        <br/><br/><img src='https://geosaurus.maps.arcgis.com/home/js/jsapi/esri/css/images/item_type_icons/layers16.png' style=\"vertical-align:middle;\" width=16 height=16>Web Experience by nparavicini_geosaurus\n",
       "                        <br/>Last Modified: November 20, 2024\n",
       "                        <br/>0 comments, 10 views\n",
       "                    </div>\n",
       "                </div>\n",
       "                "
      ],
      "text/plain": [
       "<Item title:\"Oceanic Experience\" type:Web Experience owner:nparavicini_geosaurus>"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "experience = gis.content.get(\"b2136e64f7414d10b02b52b009d7c13b\")\n",
    "experience"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6bf87d97-eb48-4b5f-8cc1-facb7fdab800",
   "metadata": {},
   "source": [
    "When we call the `export_items()` method, there are a couple things we can dictate: \n",
    "* the location the items are exported\n",
    "* the name of the output package\n",
    "* whether to export hosted services as a File Geodatabase or Shapefile.\n",
    "\n",
    "If no path is specified, a package will be created in a temporary file path, and that path can be used for the import."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "0df3d667-9c23-4127-b1b1-69ce59dcb56d",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'/var/folders/dn/rybvglf95w710d__8w91yngc0000gn/T/tmph411_rwn/exported_content.contentexport'"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "file_path = ocm.export_items([experience])\n",
    "file_path"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3d6a5e05-8e29-4f0a-a889-4433ee53c017",
   "metadata": {},
   "source": [
    "## Step 2: Listing Items (Optional)\n",
    "\n",
    "This second step is optional and is just handy for understanding what exactly was exported into your offline package. This can be helpful for a few different reasons: you're sent a package by somebody else and you'd like to know exactly what was in it, you're exporting complicated items and want to see the full scope of dependencies, or you want to understand what is in the package so you can select a subset to import.\n",
    "\n",
    "We'll connect to our second GIS and see what was exported by the function in our first GIS."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "fc3ca216-212c-4428-a870-b81c602009ee",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'b2136e64f7414d10b02b52b009d7c13b': {'title': 'Oceanic Experience',\n",
       "  'type': 'Web Experience',\n",
       "  'created': 1721117377000,\n",
       "  'source': 'https://geosaurus.maps.arcgis.com'},\n",
       " 'bf5a039b1947422190b5812fc34af717': {'title': 'Ugly Map',\n",
       "  'type': 'Web Map',\n",
       "  'created': 1720503733000,\n",
       "  'source': 'https://geosaurus.maps.arcgis.com'},\n",
       " 'd322ea73f27844228a2f235d9615854e': {'title': 'CA/OR Airports',\n",
       "  'type': 'Feature Service',\n",
       "  'created': 1720503718000,\n",
       "  'source': 'https://geosaurus.maps.arcgis.com'}}"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "gis2 = GIS(profile=\"another_admin_profile\")\n",
    "ocm2 = gis2.content.offline\n",
    "ocm2.list_items(file_path)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9b267b6b-4a37-4ab1-b6eb-cd14095f78fb",
   "metadata": {},
   "source": [
    "As we can see, more than just our initial Web Experience got exported- the map contained within the Experience, as well as the Feature Layer displayed within the map, were also exported."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6c29295e-da19-4f05-8fd6-ccc326d445ec",
   "metadata": {},
   "source": [
    "## Step 3: Importing\n",
    "\n",
    "This is the final and most involved step. Importing the content into the target org is very straightforward, but there are a few options you have to customize the process:\n",
    "- which specific items to import from a package or all of them\n",
    "- whether to roll back all item creation on failure, or skip over failed items and continue cloning the rest\n",
    "- whether to use an `item_mapping` parameter (similar to `clone_items()`) that allows you to replace certain dependencies with existent items\n",
    "\n",
    "Let's look at a basic call, and then we'll observe the failsafe mechanisms in place when there is an issue encountered importing."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "731b43aa-076a-4345-9cbe-6bccbd00b070",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[<Item title:\"CA/OR Airports\" type:File Geodatabase owner:nparavicini>,\n",
       " <Item title:\"CA/OR Airports\" type:Feature Layer Collection owner:nparavicini>,\n",
       " <Item title:\"Ugly Map\" type:Web Map owner:nparavicini>,\n",
       " <Item title:\"Oceanic Experience\" type:Web Experience owner:nparavicini>]"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "imported = ocm2.import_content(file_path)\n",
    "imported"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "aed977b7-c8c2-4a24-a5f3-494fdb7178fd",
   "metadata": {},
   "source": [
    "As we can see, the imported content contains an extra item compared to what we started with: a File Geodatabase. We needed to store the data from the feature layer in some offline format, so that is where the file geodatabase comes from.\n",
    "\n",
    "What if we're importing a larger set of items, and some of them might have issues? For anybody familiar with `clone_items()`, one notable caveat of working with the function is that if any single item fails along the way, all items are rolled back and deleted, meaning a user has to start at square one or find an alternative. Fortunately the `import_content()` method allows users to skip by items that encounter issues. We'll show what happens when cloning a package where some of the items have been corrupted."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "0867a29f-d4e6-439e-891a-0272e3fb573a",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'b13272e4cf334436b1f86ba0137a0aa8': {'title': \"Why won't this work?\",\n",
       "  'type': 'StoryMap',\n",
       "  'created': 1745620980000,\n",
       "  'source': 'https://geosaurus.maps.arcgis.com'},\n",
       " 'cb7dd4102c51495e8b4c3a346f03f612': {'title': 'Minnesota_Private_Wells',\n",
       "  'type': 'Feature Service',\n",
       "  'created': 1751491976000,\n",
       "  'source': 'https://geosaurus.maps.arcgis.com'},\n",
       " '265de67ceed6498e8cf3fc4a9f795413': {'title': 'Minnesota_Private_Wells',\n",
       "  'type': 'File Geodatabase',\n",
       "  'created': 1751491961000,\n",
       "  'source': 'https://geosaurus.maps.arcgis.com'}}"
      ]
     },
     "execution_count": 20,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "fail_path = \"/Users/pythondemo/Downloads/ocm_failure.contentexport\"\n",
    "ocm.list_items(fail_path)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "b3329112-adf8-4abc-8ccd-9bf3caa9c5aa",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/Users/pythondemo/packages/arcgis/apps/itemgraph/_migration.py:767: RuntimeWarning: Failed to import item b13272e4cf334436b1f86ba0137a0aa8 due to error: Expected object or value. Skipping...\n",
      "  warnings.warn(\n",
      "/Users/pythondemo/packages/arcgis/apps/itemgraph/_migration.py:767: RuntimeWarning: Failed to import item cb7dd4102c51495e8b4c3a346f03f612 due to error: Trailing data. Skipping...\n",
      "  warnings.warn(\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "[<Item title:\"Minnesota_Private_Wells\" type:File Geodatabase owner:nparavicini>]"
      ]
     },
     "execution_count": 21,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "ocm2.import_content(fail_path)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1415d59e-96b2-4d46-acb4-8bbd8632990f",
   "metadata": {},
   "source": [
    "As we can see, it skipped over importing the two failed items and gave us a warning about what happened, but carried out importing the one valid one. However, if we wanted, we could also set `failure_rollback = True`, and it wouldn't have imported anything, just like `clone_items()`.\n",
    "\n",
    "A quick search tells us that those are both errors commonly associated with reading JSON files in Python, meaning that our JSON files within our package are likely corrupted- we could unzip the package with a file explorer and examine them, or examine them in our ArcGIS organization and re-export them. \n",
    "\n",
    "This is just one use case for the `OfflineContentManager`, and there are numerous other advantages to using it. Some other use cases include:\n",
    "- Being able to back up items offline, providing more safety in the event of accidental deletion or changes\n",
    "- Migration between portals, especially in cases where `clone_items()` may encounter an issue\n",
    "- Creating drafts of applications that can be uploaded and edited elsewhere\n",
    "- Migrating from one organization to many- creating one backup prevents going through repeating export logic in `clone_items()`, and speeds up the process by only doing importing\n",
    "- Exporting large sets of hosted feature services to one specific file format\n",
    "\n",
    "Happy cloning! For further resources, explore other guides in the \"Content Management\" tab on the [API for Python Documentation home page](../)."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
